< Back
# Spring OAuth2 Client + KeyCloak + React The scenario: if user hasn't authenticated, user will be redirected to authorization server (Keycloak). After user provided a credential, keycloak response with auth code to browser, then the browser redirect the auth code to a designate callback at api, which allow api to use the auth code to exchange with access token in the back channel. ```plantuml @startuml actor User as user participant "React (Browser)" as react participant "API (Springboot)" as api participant KeyCloak as keyclaok user -> react++: click protected resources react -> api++: alt Not Authenticated api --> react--++ react -> keycloak--++: Redirect user -> keycloak: Enter credential keycloak -> react--++: authorization code react -> api--++: authorization code api -> keycloak++: exchange token keycloak -> api--: access token api -> api: create session api -> react--: session id else Authenticated react -> api: with session id api <-> react: continue end @enduml ``` ## Create API - use Spring init add **web** and **OAuth2 client** as dependencies - `AppConfig` ```java package com.kone.sandbox.authlogin.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class AppConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000") .allowCredentials(true); } } ``` - `SecurityConfig` ```java package com.kone.sandbox.authlogin.config; import com.kone.sandbox.authlogin.handler.AuthenticationHandler; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Slf4j @EnableWebSecurity @AllArgsConstructor public class SecurityConfig extends WebSecurityConfigurerAdapter { private final AuthenticationHandler authenticationHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(authenticationHandler) .and() .authorizeRequests(a -> a.antMatchers("/oauth2/authorization/**").permitAll() .anyRequest().authenticated() ) .oauth2Login() .defaultSuccessUrl("http://localhost:3000", true); } } ``` - `UserDetailController` ```java package com.kone.sandbox.authlogin.controller; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class UserDetailController { @GetMapping("/user") public ResponseEntity<Map<String, Object>> userDetail(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User principal) { var result = new HashMap<String, Object>() {{ put("principal", authorizedClient.getPrincipalName()); put("principal-name", principal.getName()); put("client-id", authorizedClient.getClientRegistration().getClientId()); }}; return ResponseEntity.ok(result); } } ``` - `AuthenticationHandler` ```java package com.kone.sandbox.authlogin.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; @Component @Slf4j public class AuthenticationHandler implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.addHeader("Access-Control-Allow-Origin", "http://localhost:3000"); response.addHeader("Access-Control-Allow-Credentials", "true"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write(new ObjectMapper().writeValueAsString( Map.of("error", "unauthenticated", "message", "Probably you haven't login yet." ) )); log.info("Ended exception handling"); } } ``` - `application.yml` ```yaml spring: security: oauth2: client: registration: keycloak: client-id: crux-api client-secret: FsGjfn7QcrOZdXosTjGPhqVik4Y6smz2 authorization-grant-type: authorization_code provider: keycloak: issuer-uri: http://192.168.99.111:8180/auth/realms/wshop ``` ## UI A simple function to call `/user` endpoint could be ```typescript const fetchUserDetail = () => { fetch("http://localhost:8080/user", { credentials: "include", }) .then((x) => { if (x.status === 401) { x.json().then((v) => { login(); }); return; } x.json().then((v) => { setUserdetail(JSON.stringify(v)); }); }) .catch((err) => { console.log("error", err); setUserdetail("ERROR!!"); }); }; ``` and use it as ```typescript <button onClick={fetchUserDetail}>Fetch data</button> ```